package in.srain.cube.util.log;

import android.app.ActivityManager;
import android.content.Context;

import in.srain.cube.BuildConfig;
import in.srain.cube.app.init.InitializableOneWay;
import in.srain.cube.util.ZipUtil;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import in.srain.cube.cache.DiskFileUtils;
import in.srain.cube.concurrent.SimpleTask;
import in.srain.cube.diskcache.FileUtils;
import in.srain.cube.util.CLog;
import in.srain.cube.util.NetworkStatusManager;
import in.srain.cube.util.time.TimeData;

public class LogServant implements InitializableOneWay, Runnable {

    private static final String TAG = "LogServant";
    private static final String PATH_FOR_LOG = "logs";
    private static final String FILENAME_FOR_LOG = "_log.txt";
    private static final String FILENAME_FOR_LOG_TO_BE_UPLOADED = "log.zip";
    private static final String TMP_LOG_DIR = "tmp";
    private static final String APP_NAME = "cp";

    private static final char CHAR_NEW_LINE = '\n';
    private static final char CHAR_SLASH = '/';
    private static final char CHAR_DASH = '-';
    private static final char CHAR_TAB = '\t';
    private static final char CHAT_SPACE = ' ';
    private static final char CHAR_COMMA = ':';
    private static final char CHAR_PERIODS = '.';
    private static final char CHAR_UNDERLINE = '_';

    private static final SimpleDateFormat DATE_FORMAT_FOR_LOG_TIME = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
    private static final SimpleDateFormat DATE_FORMAT_FOR_LOG_TEMP_FILE = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final long LOG_FILE_MAX_SIZE = 1;
    private static final boolean DEBUG = false;

    private static long SHOULD_SAVE_THRESHOLD = 10;

    private static LogServant sInstance;
    private final ExecutorService mExecutorService = new ThreadPoolExecutor(0, 2,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    private final byte[] mFileAccessLock = new byte[0];
    private final byte[] mUploadLock = new byte[0];

    private File mLogFile;
    private File mLogFileToBeUploaded;
    private File mTempLogDir;

    private int mUnSavedNum = 0;

    private BufferedWriter mLogWriter;

    private String mProcessId;
    private String mProcessName;

    private LogUploader mLogUploader;

    private AtomicBoolean mIsLogFileReady = new AtomicBoolean();


    private AtomicBoolean mIsRunning = new AtomicBoolean();
    private ConcurrentLinkedQueue<LogMessage> mMessageQueue;

    private LogServant() {
        mMessageQueue = new ConcurrentLinkedQueue<>();
    }

    public synchronized static LogServant getInstance() {
        if (sInstance == null) {
            sInstance = new LogServant();
        }
        return sInstance;
    }

    private static File nextTempLogFile(File dir) {
        File[] files = dir.listFiles();
        int count = files == null ? 0 : files.length;
        String fileName = String.valueOf(count) + CHAR_UNDERLINE + DATE_FORMAT_FOR_LOG_TEMP_FILE.format(new Date()) + ".txt";
        return new File(dir, fileName);
    }

    public void setLogUploader(LogUploader logUploader) {
        mLogUploader = logUploader;
    }

    private void init(Context context) {
        mProcessName = getCurrentProcessName(context);

        mProcessId = String.valueOf(android.os.Process.myPid());
        String appName = context.getString(context.getApplicationInfo().labelRes) + CHAR_DASH + BuildConfig.VERSION_NAME;

        CLog.d(TAG, "app name: %s", appName);

        initLogFile(context);

        LogProxy.getInstance().getPlainLog().i(TAG, "LogServant is ready");
        mIsLogFileReady.set(true);
    }

    void writeLogAsync(String message, Object data) {
        LogMessage m = LogMessage.obtain(message, data);
        mMessageQueue.add(m);
        if (!mIsLogFileReady.get()) {
            LogProxy.getInstance().getPlainLog().w(TAG, "log files have not been ready yet. submit job later!");
            return;
        }
        if (mIsRunning.compareAndSet(false, true)) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().v(TAG, "submit write log message");
            }
            mExecutorService.submit(this);
        }
    }

    void setShouldSaveThreshold(long shouldSaveThreshold) {
        SHOULD_SAVE_THRESHOLD = shouldSaveThreshold;
    }

    @Override
    public void run() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().v(TAG, "begin write log message");
        }
        synchronized (mFileAccessLock) {
            while (!mMessageQueue.isEmpty()) {
                LogMessage logMessage = mMessageQueue.poll();
                writeMessage(logMessage);
                logMessage.recycle();
                mUnSavedNum++;
                if (mUnSavedNum >= SHOULD_SAVE_THRESHOLD) {
                    flushLogFile();
                    tryUpload();
                }
            }
            mFileAccessLock.notify();
            mIsRunning.set(false);
        }
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().v(TAG, "finish write log message");
        }
    }

    /**
     * ${time_iso_8601} ${client_ip} ${pid}.${rnd} ${event_key} ${payload_json_encoded}
     * sample:
     * 2018-05-07T11:48:23+0800 122.96.43.56 11824.68684 app_name mid-call {"fail":0,"service":"event_agent.pub_event","rt":0.00065803527832031}
     *
     * @param message the message is add by {@link #writeLogAsync(String message, Object data)}
     */
    private void writeMessage(LogMessage message) {
        if (mLogWriter == null) {
            return;
        }
        try {
            mLogWriter.write(DATE_FORMAT_FOR_LOG_TIME.format(TimeData.getInstance().getServerTime(message.getTime())));
            mLogWriter.write(CHAT_SPACE);
            mLogWriter.write(NetworkStatusManager.getIpAddress());
            mLogWriter.write(CHAT_SPACE);
            mLogWriter.write(mProcessId);
            mLogWriter.write(CHAR_PERIODS);
            mLogWriter.write(message.getTid());
            mLogWriter.write(CHAT_SPACE);
            mLogWriter.write(APP_NAME);
            mLogWriter.write(CHAT_SPACE);
            mLogWriter.write(message.getMessage());
            if (message.getMessageData() != null) {
                mLogWriter.write(CHAT_SPACE);
                mLogWriter.write(message.getMessageData().toString());
            }
            mLogWriter.write(CHAR_NEW_LINE);
        } catch (IOException e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "doWriteWork", e);
        }
    }

    private void flushLogFile() {
        if (mLogWriter == null) {
            return;
        }
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "flushLogFile: unsaved operation num: %s", mUnSavedNum);
        }
        mUnSavedNum = 0;
        try {
            mLogWriter.flush();
        } catch (Exception e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "flushLogFile", e);
        }
    }

    private String getCurrentProcessName(Context context) {
        final String nullName = "";
        int pid = android.os.Process.myPid();
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        if (manager == null) {
            return nullName;
        }
        List<ActivityManager.RunningAppProcessInfo> runningProcessList = manager.getRunningAppProcesses();
        if (runningProcessList == null) {
            return nullName;
        }
        for (ActivityManager.RunningAppProcessInfo processInfo : runningProcessList) {
            if (processInfo.pid == pid) {
                return processInfo.processName;
            }
        }
        return nullName;
    }

    private void initLogFile(Context context) {

        DiskFileUtils.CacheDirInfo info = DiskFileUtils.getDiskCacheDir(context, PATH_FOR_LOG, 1024);

        mLogFile = new File(info.path, mProcessName.replace(CHAR_COMMA, CHAR_UNDERLINE) + FILENAME_FOR_LOG);
        mLogFileToBeUploaded = new File(info.path, FILENAME_FOR_LOG_TO_BE_UPLOADED);
        mTempLogDir = new File(info.path, TMP_LOG_DIR);
        if (!mTempLogDir.exists()) {
            mTempLogDir.mkdirs();
        }

        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "initLogFile");
            LogProxy.getInstance().getPlainLog().d(TAG, "%s size: %s", mLogFile.getAbsoluteFile(), mLogFile.length());
            LogProxy.getInstance().getPlainLog().d(TAG, "%s size: %s", mLogFileToBeUploaded.getAbsoluteFile(), mLogFileToBeUploaded.length());
        }

        if (isLogSizeNotEnough()) {
            initLogFileWriter();
        }
    }

    /**
     * Sync Compress all the log under temporary directory, upload the zip file, clear all the temporary files
     */
    private void compressUploadThenClearSync() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "compressUploadThenClearSync");
        }

        synchronized (mUploadLock) {
            compressLog();
            if (uploadLog()) {
                clearTemporaryLog();
                if (DEBUG) {
                    LogProxy.getInstance().getPlainLog().d(TAG, "Upload log success");
                }
            } else {
                if (DEBUG) {
                    LogProxy.getInstance().getPlainLog().d(TAG, "Upload log fail");
                }
            }
        }
    }

    /**
     * Compress all the log under temporary directory, upload the zip file, clear all the temporary files
     *
     * @param uploadCallback the call back will be call after {@link SimpleTask#onFinish(boolean canceled)}
     */
    private void compressUploadThenClearAsync(final UploadCallback uploadCallback) {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "compressUploadThenClearAsync");
        }
        mExecutorService.submit(new SimpleTask() {

            private boolean mSuccess = false;

            @Override
            public void doInBackground() {
                synchronized (mUploadLock) {
                    compressLog();
                    if (uploadLog()) {
                        clearTemporaryLog();
                        mSuccess = true;
                        if (DEBUG) {
                            LogProxy.getInstance().getPlainLog().d(TAG, "Upload log success");
                        }
                    } else {
                        if (DEBUG) {
                            LogProxy.getInstance().getPlainLog().d(TAG, "Upload log fail");
                        }
                    }
                }
            }

            @Override
            public void onFinish(boolean canceled) {
                if (uploadCallback != null) {
                    uploadCallback.afterUpload(mSuccess);
                }
            }
        });
    }

    private boolean isLogSizeNotEnough() {
        if (mLogFile.length() < LOG_FILE_MAX_SIZE) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "checkShouldUpload: file size is not big enough: %s %s", mLogFile.length(), LOG_FILE_MAX_SIZE);
            }
            return true;
        }
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "checkShouldUpload: should upload, the file size is: %s", mLogFile.length());
        }
        return false;
    }

    private void tryUpload() {
        if (isLogSizeNotEnough()) {
            return;
        }

        reCreateFileAndFileWriter();
        compressUploadThenClearSync();
    }

    private void reCreateFileAndFileWriter() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "reCreateFileAndFileWriter");
        }
        tryToCloseLogWriter();
        moveLogToTemporaryPath();
        initLogFileWriter();
    }

    private void moveLogToTemporaryPath() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "moveLogToTemporaryPath");
        }
        if (mLogFile == null || !mLogFile.exists() || mLogFile.length() == 0) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "Log file is not exists or the size is 0: %s", mLogFile);
            }
            return;
        }
        if (mTempLogDir == null || !mTempLogDir.exists() || mTempLogDir.length() == 0) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "Temp Log file is not exists or the size is 0: %s", mTempLogDir);
            }
            return;
        }
        File file = nextTempLogFile(mTempLogDir);
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "Moved log file from %s to %s", mLogFile, file);
        }
        mLogFile.renameTo(file);
    }

    private void compressLog() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "compressLog");
        }
        File[] files = mTempLogDir.listFiles();
        if (!mTempLogDir.exists() || mTempLogDir.length() == 0 || files == null || files.length == 0) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "Temp log dir is not exists or empty: %s", files == null ? 0 : files.length);
            }
            return;
        }
        ZipUtil.zipFile(mTempLogDir, mLogFileToBeUploaded);
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "File after zip: %s, %s", mLogFileToBeUploaded, mLogFileToBeUploaded.length());
        }
    }

    private void initLogFileWriter() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "initLogFileWriter");
        }
        try {
            mLogWriter = new BufferedWriter(new FileWriter(mLogFile, true), IO_BUFFER_SIZE);
        } catch (IOException e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "initLogFileWriter error", e);
        }
    }

    /**
     * Synchronously update the log to server
     */
    private boolean uploadLog() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "Begin upload log: %s", mLogFileToBeUploaded);
        }
        if (mLogUploader == null) {
            LogProxy.getInstance().getPlainLog().e(TAG, "mLogUploader is null, stop uploading.");
            return false;
        }
        // The file is not exist or empty.
        if (!mLogFileToBeUploaded.exists() || mLogFileToBeUploaded.length() == 0) {
            return false;
        }
        try {
            return mLogUploader.upload(mLogFileToBeUploaded);
        } catch (Exception e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "uploadLog error: %s", e);
        }
        return false;
    }

    private void clearTemporaryLog() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "clearTemporaryLog");
        }
        // remove all zip file
        mLogFileToBeUploaded.delete();
        // remove all temporary files
        try {
            FileUtils.deleteDirectoryQuickly(mTempLogDir);
        } catch (Exception e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "clearTemporaryLog", e);
        }
    }

    private void tryToCloseLogWriter() {
        if (DEBUG) {
            LogProxy.getInstance().getPlainLog().d(TAG, "tryToCloseLogWriter");
        }
        if (mLogWriter == null) {
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "The mLogWriter is null.");
            }
            return;
        }
        try {
            mLogWriter.close();
            if (DEBUG) {
                LogProxy.getInstance().getPlainLog().d(TAG, "closed mLogWriter successfully");
            }
        } catch (Exception e) {
            LogProxy.getInstance().getPlainLog().e(TAG, "try to close log file writer error", e);
        }
    }

    @Override
    public void initiateAsyncOneWay(Context context) {
        init(context);
    }
}
